# -*- coding: utf-8 -*-
import json
import os
import hashlib
from PySide6.QtWidgets import QVBoxLayout
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtCore import QUrl, QTimer, QObject, Signal, Slot
from PySide6.QtWebChannel import QWebChannel
from ui.components.base.widget import TgBaseWidget
import pandas as pd
from loguru import logger


class Bridge(QObject):
    # Signal to notify Python about period change
    periodChanged = Signal(str)
    indicatorChanged = Signal(str)

    @Slot(str, result=bool)
    def onPeriodClicked(self, period: str):
        """This slot is called from JavaScript when a period button is clicked."""
        self.periodChanged.emit(period)
        return True

    @Slot(str, result=bool)
    def onIndicatorChanged(self, indicator: str):
        """This slot is called from JavaScript when an indicator is selected."""
        self.indicatorChanged.emit(indicator)
        return True


class TgTradingViewChart(TgBaseWidget):
    def __init__(self, parent=None, symbol: str = None, width: int = 800, height: int = 600, period_changed_callback: callable = None, show_minutes_buttons: bool = True):
        super().__init__(parent)
        self.symbol = symbol
        self.width = width
        self.height = height
        self.web_view = None
        self.chart_data = None
        self.original_df = None
        self.daily_df_cache = None  # 单独缓存日线数据用于合成其他周期
        self.lines_data = {}
        self._page_loaded = False
        self._ready_check_attempts = 0
        self._max_ready_check_attempts = 50  # 50 * 100ms = 5s timeout
        self.period_changed_callback = period_changed_callback
        self.current_period = '1d'
        self.current_indicator = 'none'
        self.indicator_lines = []
        self.indicator_cache = {}
        self.data_hash = None
        self.bridge = None
        self.channel = None
        self.has_volume = False
        self.show_minutes_buttons = show_minutes_buttons
        self.initialize()

    def setup_ui(self):
        """
        Sets up the user interface for the chart widget.
        """
        self.layout = QVBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)

        # Create QWebEngineView
        self.web_view = QWebEngineView(self)
        self.web_view.setMinimumSize(self.width, self.height)
        # 确保WebView填充整个可用空间
        from PySide6.QtWidgets import QSizePolicy
        self.web_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        # 设置固定大小以确保正确显示
        self.web_view.resize(self.width, self.height)

        # Setup QWebChannel for JS <-> Python communication
        self.bridge = Bridge(self)
        self.bridge.periodChanged.connect(self._handle_period_change)
        self.bridge.indicatorChanged.connect(self._handle_indicator_change)

        self.channel = QWebChannel(self.web_view.page())
        self.web_view.page().setWebChannel(self.channel)
        self.channel.registerObject("pyBridge", self.bridge)

        # Configure settings
        settings = self.web_view.page().settings()
        settings.setAttribute(settings.WebAttribute.JavascriptEnabled, True)
        settings.setAttribute(settings.WebAttribute.WebGLEnabled, True)

        # Connect load finished signal
        self.web_view.loadFinished.connect(self._on_load_finished)
        
        # Load the HTML template
        self._load_chart_html()
        
        self.layout.addWidget(self.web_view)
        
        # 确保布局正确应用
        self.setLayout(self.layout)

    def cleanup(self):
        """
        Properly clean up resources to prevent crashes on re-creation.
        """
        logger.info("Cleaning up TgTradingViewChart...")
        
        # Reset state flags
        self._page_loaded = False
        self._ready_check_attempts = 0
        
        if self.web_view:
            try:
                # Stop any ongoing loading or scripts
                self.web_view.stop()
                
                # Disconnect all signals to avoid any race conditions during cleanup
                try:
                    self.web_view.loadFinished.disconnect()
                except (TypeError, RuntimeError):
                    pass
                
                if self.bridge:
                    try:
                        self.bridge.periodChanged.disconnect()
                        self.bridge.indicatorChanged.disconnect()
                    except (TypeError, RuntimeError) as e:
                        logger.exception(e)

                # Unregister the bridge object
                if self.channel and self.bridge:
                    try:
                        self.channel.deregisterObject(self.bridge)
                    except (TypeError, RuntimeError) as e:
                        logger.exception(e)

                # Clear the web page content first
                self.web_view.setHtml("")
                
                # Get the page and clear it
                page = self.web_view.page()
                if page:
                    # Clear the web channel
                    page.setWebChannel(None)
                    # Delete the page
                    page.deleteLater()
                
                # Remove from layout if it has a parent
                if self.web_view.parent():
                    layout = self.layout
                    if layout:
                        layout.removeWidget(self.web_view)
                
                # Delete the web view
                self.web_view.deleteLater()
                
            except Exception as e:
                logger.exception(e)
            
            # Clear references
            self.web_view = None
            self.channel = None
            self.bridge = None
            self.chart_data = None
            self.original_df = None
            self.daily_df_cache = None
            self.lines_data = {}
            self.indicator_cache = {}
            self.data_hash = None
            
            logger.info("TgTradingViewChart cleaned up.")

    def _generate_data_hash(self, df: pd.DataFrame) -> str:
        """Generate a hash for the dataframe to use as cache key"""
        if df is None or df.empty:
            return ""
        
        try:
            # Create a more reliable hash based on data shape and content
            data_str = f"shape:{df.shape[0]}x{df.shape[1]}"
            
            # Add column names
            data_str += f"_cols:{','.join(df.columns.tolist())}"
            
            # For non-empty dataframes, use first and last values for hash
            if not df.empty:
                # Use numeric columns for hash calculation
                numeric_cols = df.select_dtypes(include=['number']).columns
                if len(numeric_cols) > 0:
                    first_row = df[numeric_cols].iloc[0]
                    last_row = df[numeric_cols].iloc[-1]
                    data_str += f"_first:{first_row.sum()}_last:{last_row.sum()}"
                    
                # Add data length
                data_str += f"_len:{len(df)}"
            
            hash_value = hashlib.md5(data_str.encode()).hexdigest()
            logger.info(f"生成数据hash: {hash_value} (基于: {data_str[:100]}...)")
            return hash_value
            
        except Exception as e:
            logger.exception(e)
            # Fallback to simple hash
            return hashlib.md5(f"{df.shape}_{len(df) if not df.empty else 0}".encode()).hexdigest()

    def _get_cache_key(self, indicator: str) -> str:
        """Generate cache key for indicator based on data hash and indicator type"""
        base_hash = self.data_hash if self.data_hash is not None else "no_hash"
        sym = self.symbol if self.symbol else "no_symbol"
        return f"{sym}_{base_hash}_{indicator}"

    def _get_cached_indicator(self, indicator: str):
        """Get cached indicator data if available"""
        cache_key = self._get_cache_key(indicator)
        cached_data = self.indicator_cache.get(cache_key)
        logger.info(f"查找缓存: indicator={indicator}, cache_key={cache_key}, found={cached_data is not None}")
        logger.info(f"当前缓存keys: {list(self.indicator_cache.keys())}")
        return cached_data

    def _cache_indicator(self, indicator: str, data):
        """Cache calculated indicator data"""
        cache_key = self._get_cache_key(indicator)
        self.indicator_cache[cache_key] = data
        logger.info(f"缓存指标数据: indicator={indicator}, cache_key={cache_key}")
        logger.info(f"缓存后keys: {list(self.indicator_cache.keys())}")

    def _clear_indicator_cache(self):
        """Clear all cached indicators"""
        self.indicator_cache.clear()

    def _handle_period_change(self, period: str):
        """Handles the period change signal from the JS bridge."""
        # Check if we're in the process of cleaning up
        if not self.web_view or self._ready_check_attempts >= self._max_ready_check_attempts:
            return
            
        self.current_period = period
        try:
            if self.period_changed_callback and callable(self.period_changed_callback):
                # For synthesized periods, use cached daily data if available
                if period in ['1w', '1M', '1Y']:
                    if self.daily_df_cache is not None and not self.daily_df_cache.empty:
                        # Use cached daily data to synthesize the requested period data
                        if period == '1w':
                            new_df = self._synthesize_weekly_data(self.daily_df_cache)
                        elif period == '1M':
                            new_df = self._synthesize_monthly_data(self.daily_df_cache)
                        elif period == '1Y':
                            new_df = self._synthesize_yearly_data(self.daily_df_cache)
                        
                        if not new_df.empty:
                            self.set_data(new_df)
                    else:
                        # No cached daily data, fetch it first
                        daily_df = self.period_changed_callback(self.symbol, '1d')
                        if daily_df is not None:
                            # Store the daily data for future use
                            self.daily_df_cache = daily_df.copy()
                            # Synthesize the requested period data
                            if period == '1w':
                                new_df = self._synthesize_weekly_data(daily_df)
                            elif period == '1M':
                                new_df = self._synthesize_monthly_data(daily_df)
                            elif period == '1Y':
                                new_df = self._synthesize_yearly_data(daily_df)
                            
                            if not new_df.empty:
                                self.set_data(new_df)
                else:
                    # Regular period, use callback directly
                    new_df = self.period_changed_callback(self.symbol, period)
                    if new_df is not None:
                        # If this is daily data, cache it
                        if period == '1d':
                            self.daily_df_cache = new_df.copy()
                        self.set_data(new_df)
            else:
                logger.info(f"No callback found for period change.")
        except Exception as e:
            import traceback
            logger.exception(e)

    def _handle_indicator_change(self, indicator: str):
        self.current_indicator = indicator
        self._update_indicators()

    def _update_indicators(self):
        if self.original_df is None or self.original_df.empty:
            return

        self._clear_indicator_lines()

        # 如果指标为"none"，直接返回，不需要缓存
        if self.current_indicator == 'none':
            return

        # Check if indicator data is cached
        cached_data = self._get_cached_indicator(self.current_indicator)
        if cached_data is not None:
            # Use cached data
            logger.info(f"使用缓存的指标数据: {self.current_indicator}")
            self._apply_cached_indicator_data(cached_data)
            return

        logger.info(f"计算新的指标数据: {self.current_indicator}")
        # Calculate and cache new indicator data
        if self.current_indicator == 'ma':
            indicator_data = self._calculate_moving_averages()
            self._cache_indicator(self.current_indicator, indicator_data)
            self._draw_moving_averages_from_data(indicator_data)
        elif self.current_indicator == 'bb':
            indicator_data = self._calculate_bollinger_bands()
            self._cache_indicator(self.current_indicator, indicator_data)
            self._draw_bollinger_bands_from_data(indicator_data)
        elif self.current_indicator == 'dc':
            indicator_data = self._calculate_donchian_channel()
            self._cache_indicator(self.current_indicator, indicator_data)
            self._draw_donchian_channel_from_data(indicator_data)

    def _clear_indicator_lines(self):
        if self.web_view and self.indicator_lines:
            js_code = "window.clearAllLines();"
            self.web_view.page().runJavaScript(js_code)
            self.indicator_lines = []

    def _apply_cached_indicator_data(self, cached_data):
        """Apply cached indicator data to the chart"""
        for line_name, line_data in cached_data.items():
            color = line_data['color']
            width = line_data['width']
            data = line_data['data']
            
            self.create_line(line_name, color, width)
            self.update_line(line_name, data)
            self.indicator_lines.append(line_name)

    def _calculate_moving_averages(self):
        """Calculate moving averages and return data for caching"""
        df = self.original_df
        periods = [5, 10, 20]
        colors = ['#FF5733', '#33CFFF', '#FFC300']
        
        cached_data = {}
        for i, period in enumerate(periods):
            ma_name = f"MA{period}"
            ma_series = df['close'].rolling(window=period).mean()
            line_df = ma_series.to_frame(name='value')

            if 'date' in df.columns:
                line_df = line_df.join(df[['date']])
            
            line_df.dropna(inplace=True)
            
            cached_data[ma_name] = {
                'color': colors[i],
                'width': 1,
                'data': line_df
            }
            
        return cached_data

    def _draw_moving_averages_from_data(self, indicator_data):
        """Draw moving averages from cached data"""
        for line_name, line_data in indicator_data.items():
            self.create_line(line_name, line_data['color'], line_data['width'])
            self.update_line(line_name, line_data['data'])
            self.indicator_lines.append(line_name)

    def _calculate_bollinger_bands(self):
        """Calculate Bollinger Bands and return data for caching"""
        df = self.original_df
        period = 20
        std_dev = 2
        
        ma = df['close'].rolling(window=period).mean()
        std = df['close'].rolling(window=period).std()
        
        upper_band = (ma + (std * std_dev)).to_frame(name='value')
        lower_band = (ma - (std * std_dev)).to_frame(name='value')
        middle_band = ma.to_frame(name='value')

        if 'date' in df.columns:
            upper_band = upper_band.join(df[['date']])
            lower_band = lower_band.join(df[['date']])
            middle_band = middle_band.join(df[['date']])

        upper_band.dropna(inplace=True)
        lower_band.dropna(inplace=True)
        middle_band.dropna(inplace=True)

        cached_data = {
            "BB_Upper": {
                'color': "rgba(68, 142, 247, 0.5)",
                'width': 1,
                'data': upper_band
            },
            "BB_Middle": {
                'color': "rgba(255, 165, 0, 0.8)",
                'width': 1,
                'data': middle_band
            },
            "BB_Lower": {
                'color': "rgba(68, 142, 247, 0.5)",
                'width': 1,
                'data': lower_band
            }
        }
        
        return cached_data

    def _draw_bollinger_bands_from_data(self, indicator_data):
        """Draw Bollinger Bands from cached data"""
        for line_name, line_data in indicator_data.items():
            self.create_line(line_name, line_data['color'], line_data['width'])
            self.update_line(line_name, line_data['data'])
            self.indicator_lines.append(line_name)

    def _calculate_donchian_channel(self):
        """Calculate Donchian Channel and return data for caching"""
        df = self.original_df
        period = 20

        upper_channel = df['high'].rolling(window=period).max().to_frame(name='value')
        lower_channel = df['low'].rolling(window=period).min().to_frame(name='value')
        middle_channel = ((df['high'].rolling(window=period).max() + df['low'].rolling(window=period).min()) / 2).to_frame(name='value')

        if 'date' in df.columns:
            upper_channel = upper_channel.join(df[['date']])
            lower_channel = lower_channel.join(df[['date']])
            middle_channel = middle_channel.join(df[['date']])

        upper_channel.dropna(inplace=True)
        lower_channel.dropna(inplace=True)
        middle_channel.dropna(inplace=True)

        cached_data = {
            "DC_Upper": {
                'color': "#FF6347",
                'width': 1,
                'data': upper_channel
            },
            "DC_Middle": {
                'color': "#FFD700",
                'width': 1,
                'data': middle_channel
            },
            "DC_Lower": {
                'color': "#32CD32",
                'width': 1,
                'data': lower_channel
            }
        }
        
        return cached_data

    def _draw_donchian_channel_from_data(self, indicator_data):
        """Draw Donchian Channel from cached data"""
        for line_name, line_data in indicator_data.items():
            self.create_line(line_name, line_data['color'], line_data['width'])
            self.update_line(line_name, line_data['data'])
            self.indicator_lines.append(line_name)

    def _draw_moving_averages(self):
        df = self.original_df
        periods = [5, 10, 20]
        colors = ['#FF5733', '#33CFFF', '#FFC300']
        
        for i, period in enumerate(periods):
            ma_name = f"MA{period}"
            ma_series = df['close'].rolling(window=period).mean()
            line_df = ma_series.to_frame(name='value')

            if 'date' in df.columns:
                line_df = line_df.join(df[['date']])
            
            line_df.dropna(inplace=True)

            self.create_line(ma_name, colors[i], 1)
            self.update_line(ma_name, line_df)
            self.indicator_lines.append(ma_name)

    def _draw_bollinger_bands(self):
        df = self.original_df
        period = 20
        std_dev = 2
        
        ma = df['close'].rolling(window=period).mean()
        std = df['close'].rolling(window=period).std()
        
        upper_band = (ma + (std * std_dev)).to_frame(name='value')
        lower_band = (ma - (std * std_dev)).to_frame(name='value')

        if 'date' in df.columns:
            upper_band = upper_band.join(df[['date']])
            lower_band = lower_band.join(df[['date']])

        upper_band.dropna(inplace=True)
        lower_band.dropna(inplace=True)

        middle_band = ma.to_frame(name='value')

        if 'date' in df.columns:
            middle_band = middle_band.join(df[['date']])
        
        middle_band.dropna(inplace=True)

        self.create_line("BB_Upper", "rgba(68, 142, 247, 0.5)", 1)
        self.update_line("BB_Upper", upper_band)
        self.indicator_lines.append("BB_Upper")

        self.create_line("BB_Middle", "rgba(255, 165, 0, 0.8)", 1)
        self.update_line("BB_Middle", middle_band)
        self.indicator_lines.append("BB_Middle")

        self.create_line("BB_Lower", "rgba(68, 142, 247, 0.5)", 1)
        self.update_line("BB_Lower", lower_band)
        self.indicator_lines.append("BB_Lower")

    def _draw_donchian_channel(self):
        df = self.original_df
        period = 20

        upper_channel = df['high'].rolling(window=period).max().to_frame(name='value')
        lower_channel = df['low'].rolling(window=period).min().to_frame(name='value')
        middle_channel = ((df['high'].rolling(window=period).max() + df['low'].rolling(window=period).min()) / 2).to_frame(name='value')

        if 'date' in df.columns:
            upper_channel = upper_channel.join(df[['date']])
            lower_channel = lower_channel.join(df[['date']])
            middle_channel = middle_channel.join(df[['date']])

        upper_channel.dropna(inplace=True)
        lower_channel.dropna(inplace=True)
        middle_channel.dropna(inplace=True)

        self.create_line("DC_Upper", "#FF6347", 1)
        self.update_line("DC_Upper", upper_channel)
        self.indicator_lines.append("DC_Upper")

        self.create_line("DC_Middle", "#FFD700", 1)
        self.update_line("DC_Middle", middle_channel)
        self.indicator_lines.append("DC_Middle")

        self.create_line("DC_Lower", "#32CD32", 1)
        self.update_line("DC_Lower", lower_channel)
        self.indicator_lines.append("DC_Lower")

    def _load_chart_html(self):
        """
        Load the HTML template with TradingView Lightweight Charts
        """
        html_content = self._get_chart_html_template()
        
        # Set the base URL to the directory of this script, so the HTML can find the local JS file.
        script_dir = os.path.dirname(os.path.abspath(__file__))
        base_url = QUrl.fromLocalFile(script_dir + os.path.sep)
        
        self.web_view.setHtml(html_content, baseUrl=base_url)

    def _get_chart_html_template(self):
        """
        Returns the HTML template for the chart.
        Uses the 'onload' attribute of the script tag to robustly trigger initialization.
        """
        # We always show period buttons now, just different sets
        periods_class = ""
        show_minutes_buttons_js = str(self.show_minutes_buttons).lower()
        return f"""
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>TradingView Chart</title>
    <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
    <style>
        html, body {{
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            background-color: #ffffff;
            width: 100%;
            height: 100%;
            overflow: hidden;
        }}
        #chart-container {{
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
        }}
        #loading {{
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100%;
            font-size: 18px;
            color: #666;
        }}
        #periods {{
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 20;
            display: flex;
            gap: 5px;
        }}
        #periods.hidden {{
            display: none;
        }}
        #periods button {{
            background-color: rgba(240, 240, 240, 0.9);
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 5px 10px;
            cursor: pointer;
            font-size: 12px;
        }}
        #periods button:hover {{
            background-color: rgba(224, 224, 224, 0.9);
        }}
        #periods button.active {{
            background-color: rgba(208, 208, 208, 0.9);
            font-weight: bold;
        }}
        #indicators {{
            position: absolute;
            top: 10px;
            right: 10px;
            z-index: 20;
        }}
        #indicators select {{
            background-color: rgba(240, 240, 240, 0.9);
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 5px;
            font-size: 12px;
        }}
        #ohlc-info {{
            position: absolute;
            top: 50px;
            left: 10px;
            z-index: 20;
            background-color: rgba(255, 255, 255, 0.9);
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 8px;
            font-size: 12px;
            font-family: monospace;
            display: none;
        }}
        #ohlc-info .ohlc-row {{
            margin: 2px 0;
        }}
        #ohlc-info .label {{
            color: #666;
            display: inline-block;
            width: 40px;
        }}
        #ohlc-info .value {{
            font-weight: bold;
        }}
        #ohlc-info .up {{
            color: #ef5350;
        }}
        #ohlc-info .down {{
            color: #26a69a;
        }}
    </style>
</head>
<body>
    <div id="loading">Loading chart...</div>
    <div id="chart-container" style="display: none;">
        <div id="periods" class="{periods_class}"></div>
        <div id="indicators">
           <select id="indicator-select">
               <option value="none">无指标</option>
               <option value="ma">移动平均线 (MA)</option>
               <option value="bb">布林带 (BB)</option>
               <option value="dc">唐奇安通道 (DC)</option>
           </select>
        </div>
        <div id="ohlc-info">
            <div class="ohlc-row">
                <span class="label">O:</span>
                <span class="value" id="open-value">--</span>
            </div>
            <div class="ohlc-row">
                <span class="label">H:</span>
                <span class="value" id="high-value">--</span>
            </div>
            <div class="ohlc-row">
                <span class="label">L:</span>
                <span class="value" id="low-value">--</span>
            </div>
            <div class="ohlc-row">
                <span class="label">C:</span>
                <span class="value" id="close-value">--</span>
            </div>
            <div class="ohlc-row">
                <span class="label">涨跌:</span>
                <span class="value" id="change-value">--</span>
            </div>
        </div>
    </div>
    
    <script>
        // Global variables
        let chart = null;
        let candlestickSeries = null;
        let volumeSeries = null;
        let lineSeries = {{}};
        let pyBridge = null;
        let chartData = [];

        // Function to update OHLC display
        function updateOHLC(param) {{
            const ohlcInfo = document.getElementById('ohlc-info');
            
            if (param.time && chartData.length > 0) {{
                // Find the data point for the current time
                const dataPoint = chartData.find(item => item.time === param.time);
                
                if (dataPoint) {{
                    const isUp = dataPoint.close >= dataPoint.open;
                    const colorClass = isUp ? 'up' : 'down';
                    
                    document.getElementById('open-value').textContent = dataPoint.open.toFixed(2);
                    document.getElementById('high-value').textContent = dataPoint.high.toFixed(2);
                    document.getElementById('low-value').textContent = dataPoint.low.toFixed(2);
                    document.getElementById('close-value').textContent = dataPoint.close.toFixed(2);
                    
                    // Apply color to close value
                    const closeElement = document.getElementById('close-value');
                    closeElement.className = `value ${{colorClass}}`;
                    
                    // Update change percentage
                    const changeElement = document.getElementById('change-value');
                    if (dataPoint.changePct !== undefined && !isNaN(dataPoint.changePct)) {{
                        const changeText = (dataPoint.changePct >= 0 ? '+' : '') + dataPoint.changePct.toFixed(2) + '%';
                        changeElement.textContent = changeText;
                        changeElement.className = `value ${{dataPoint.changePct >= 0 ? 'up' : 'down'}}`;
                    }} else {{
                        changeElement.textContent = '--';
                        changeElement.className = 'value';
                    }}
                    
                    ohlcInfo.style.display = 'block';
                }} else {{
                    ohlcInfo.style.display = 'none';
                }}
            }} else {{
                ohlcInfo.style.display = 'none';
            }}
        }}

        // Define the initChart function in the global scope first.
        function initChart() {{
            document.getElementById('loading').style.display = 'none';
            document.getElementById('chart-container').style.display = 'block';
            
            // Get container dimensions
            const container = document.getElementById('chart-container');
            const containerRect = container.getBoundingClientRect();
            
            chart = LightweightCharts.createChart(document.getElementById('chart-container'), {{
                width: containerRect.width,
                height: containerRect.height,
                layout: {{
                    backgroundColor: '#ffffff',
                    textColor: '#333',
                    fontSize: 12
                }},
                grid: {{
                    vertLines: {{ color: '#e1e1e1' }},
                    horzLines: {{ color: '#e1e1e1' }}
                }},
                crosshair: {{
                    mode: LightweightCharts.CrosshairMode.Normal,
                    vertLine: {{
                        labelVisible: true
                    }},
                    horzLine: {{
                        labelVisible: true
                    }}
                }},
                rightPriceScale: {{
                    borderColor: '#cccccc',
                    scaleMargins: {{ top: 0.05, bottom: 0.15 }}
                }},
                timeScale: {{
                    borderColor: '#cccccc',
                    timeVisible: true,
                    secondsVisible: false
                }},
                handleScroll: {{
                    mouseWheel: true,
                    pressedMouseMove: true,
                }},
                handleScale: {{
                    axisPressedMouseMove: true,
                    mouseWheel: true,
                    pinch: true,
                }},
            }});

            candlestickSeries = chart.addCandlestickSeries({{
                upColor: '#ef5350', downColor: '#26a69a', borderVisible: false,
                wickUpColor: '#ef5350', wickDownColor: '#26a69a',
                priceFormat: {{
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                }}
            }});

            // Subscribe to crosshair move events to show OHLC data
            chart.subscribeCrosshairMove(updateOHLC);

            new QWebChannel(qt.webChannelTransport, function(channel) {{
                pyBridge = channel.objects.pyBridge;

                const showMinutesButtons = {show_minutes_buttons_js};
                
                const periods = showMinutesButtons ? {{
                    '日线': '1d',
                    '周线': '1w',
                    '月线': '1M',
                    '年线': '1Y',
                    '30分钟': '30m',
                    '5分钟': '5m',
                    '1分钟': '1m'
                }} : {{
                    '日线': '1d',
                    '周线': '1w',
                    '月线': '1M',
                    '年线': '1Y'
                }};
                const periodsContainer = document.getElementById('periods');
                
                // Always show period buttons, but different sets based on showMinutesButtons
                let firstButton = true;
                Object.keys(periods).forEach(label => {{
                        const button = document.createElement('button');
                        button.innerText = label;
                        button.onclick = function() {{
                            try {{
                                if (pyBridge && pyBridge.onPeriodClicked) {{
                                    pyBridge.onPeriodClicked(periods[label]);
                                }}
                                // Update active button styling
                                const buttons = document.querySelectorAll('#periods button');
                                if (buttons) {{
                                    buttons.forEach(btn => btn.classList.remove('active'));
                                }}
                                this.classList.add('active');
                            }} catch (error) {{
                                console.error("Error in period button click:", error);
                            }}
                        }};
                        periodsContainer.appendChild(button);
                        if (firstButton) {{
                            button.classList.add('active');
                            firstButton = false;
                        }}
                    }});

                document.getElementById('indicator-select').addEventListener('change', function() {{
                    if (pyBridge && pyBridge.onIndicatorChanged) {{
                        pyBridge.onIndicatorChanged(this.value);
                    }}
                }});
            }});

            window.addEventListener('resize', () => {{
                if (chart) {{
                    const container = document.getElementById('chart-container');
                    const containerRect = container.getBoundingClientRect();
                    chart.applyOptions({{
                        width: containerRect.width,
                        height: containerRect.height
                    }});
                }}
            }});

            window.setChartData = function(data, period, hasVolume) {{
                console.log("Received data for chart:", data ? data.length : 0, "points for period", period, "hasVolume:", hasVolume);
                
                // Store chart data for OHLC display
                chartData = data || [];

                const isIntraday = period && !period.endsWith('d');

                // Update chart layout based on volume presence
                if (chart) {{
                    chart.applyOptions({{
                        timeScale: {{
                            borderColor: '#cccccc',
                            timeVisible: isIntraday,
                            secondsVisible: isIntraday,
                        }},
                        rightPriceScale: {{
                            borderColor: '#cccccc',
                            scaleMargins: {{ 
                                top: 0.05, 
                                bottom: hasVolume ? 0.15 : 0.05 
                            }}
                        }}
                    }});
                }}

                // Create or remove volume series based on data
                if (hasVolume && !volumeSeries) {{
                    volumeSeries = chart.addHistogramSeries({{
                        color: '#26a69a',
                        priceFormat: {{
                            type: 'volume',
                        }},
                        priceScaleId: 'volume',
                        scaleMargins: {{
                            top: 0.9,
                            bottom: 0,
                        }},
                    }});
                    
                    // 设置volume价格刻度到右侧独立显示
                    chart.priceScale('volume').applyOptions({{
                        scaleMargins: {{
                            top: 0.9,
                            bottom: 0,
                        }},
                    }});
                }} else if (!hasVolume && volumeSeries) {{
                    chart.removeSeries(volumeSeries);
                    volumeSeries = null;
                }}

                if (data && data.length > 0) {{
                    console.log("Sample data points:", data.slice(0, 3));
                }}
                
                if (candlestickSeries) {{
                    try {{
                        // Separate volume data if present
                        const candleData = data.map(item => ({{
                            time: item.time,
                            open: item.open,
                            high: item.high,
                            low: item.low,
                            close: item.close
                        }}));
                        
                        candlestickSeries.setData(candleData || []);
                        
                        // Set volume data if available
                        if (hasVolume && volumeSeries && data) {{
                            const volumeData = data.filter(item => item.volume !== undefined).map(item => ({{
                                time: item.time,
                                value: item.volume,
                                color: item.close >= item.open ? '#ef5350' : '#26a69a'
                            }}));
                            volumeSeries.setData(volumeData);
                        }}
                        
                        if (data && data.length > 0 && chart && chart.timeScale) {{
                            if (data.length > 200) {{
                                chart.timeScale().setVisibleLogicalRange({{
                                    from: data.length - 200,
                                    to: data.length,
                                }});
                            }} else {{
                                chart.timeScale().fitContent();
                            }}
                        }}
                        console.log("Chart data set successfully");
                    }} catch (error) {{
                        console.error("Error setting chart data:", error);
                        console.error("Problematic data:", data);
                    }}
                }} else {{
                    console.error("candlestickSeries is not available");
                }}
            }};

            window.createLine = function(name, color, width) {{
                if (chart && chart.addLineSeries) {{
                    const line = chart.addLineSeries({{ color: color || 'blue', lineWidth: width || 2 }});
                    lineSeries[name] = line;
                }} else {{
                    console.error("Chart is not available for creating line series");
                }}
            }};

            window.updateLine = function(name, data) {{
                if (lineSeries && lineSeries[name]) {{
                    lineSeries[name].setData(data);
                }} else {{
                    console.warn("Line series", name, "not found");
                }}
            }};

            window.clearAllLines = function() {{
                Object.keys(lineSeries).forEach(name => {{
                    if (chart) {{
                        chart.removeSeries(lineSeries[name]);
                    }}
                }});
                lineSeries = {{}};
            }};

            window.chartReady = true;
        }}
    </script>
    <!-- Now that initChart is defined, load the external script and call it on load. -->
    <script src="lightweight-charts.standalone.production.js" onload="initChart()"></script>
</body>
</html>
        """

    def set_data(self, df: pd.DataFrame):
        """
        Sets the main OHLCV data for the chart.
        """
        # Calculate new data hash and check if data changed
        new_data_hash = self._generate_data_hash(df)
        logger.info(f"数据设置: old_hash={self.data_hash}, new_hash={new_data_hash}")
        if new_data_hash != self.data_hash:
            # Data changed, clear indicator cache
            logger.info("数据变化，清空指标缓存")
            self._clear_indicator_cache()
            self.data_hash = new_data_hash
        else:
            logger.info("数据未变化，保留缓存")
        
        self.original_df = df.copy() if df is not None else pd.DataFrame()
        
        # 如果当前周期是日线，缓存数据用于合成其他周期
        if self.current_period == '1d' and df is not None and not df.empty:
            self.daily_df_cache = df.copy()
            logger.info("缓存日线数据用于周期合成")

        if df is None or df.empty:
            self.chart_data = []
            self.has_volume = False
            if self._page_loaded:
                self._ready_check_attempts = 0
                self._check_chart_ready()
            return
            
        # Check if volume data exists
        has_volume_data = False
        volume_column = None
        for col in ['volume', 'Volume']:
            if col in df.columns:
                volume_series = df[col]
                if not volume_series.isna().all() and (volume_series > 0).any():
                    has_volume_data = True
                    volume_column = col
                    break
        
        self.has_volume = has_volume_data
            
        # Convert DataFrame to the format expected by TradingView
        chart_data = []
        
        # Calculate price change percentage vectorized
        price_change_pct = None
        if not df.empty and len(df) > 1:
            close_col = 'close' if 'close' in df.columns else 'Close'
            if close_col in df.columns:
                close_series = df[close_col]
                # Calculate percentage change from previous day
                price_change_pct = close_series.pct_change() * 100
                
        for index, row in df.iterrows():
            # Try to get time from different sources
            time_value = None
            
            # First, try to get from 'date' column
            if 'date' in row:
                if isinstance(row['date'], str):
                    try:
                        # Parse the string to datetime first
                        dt = pd.to_datetime(row['date'])
                        # Check if it has time component (not just date)
                        if dt.time() != pd.Timestamp('00:00:00').time():
                            # Has time component, use timestamp for intraday data
                            time_value = int(dt.timestamp())
                        else:
                            # Only date, use YYYY-MM-DD format for daily data
                            time_value = dt.strftime('%Y-%m-%d')
                    except:
                        time_value = row['date']
                else:
                    try:
                        # Handle datetime objects
                        dt = pd.to_datetime(row['date'])
                        # Check if it has time component (not just date)
                        if dt.time() != pd.Timestamp('00:00:00').time():
                            # Has time component, use timestamp for intraday data
                            time_value = int(dt.timestamp())
                        else:
                            # Only date, use YYYY-MM-DD format for daily data
                            time_value = dt.strftime('%Y-%m-%d')
                    except:
                        pass
            
            # If no date column, try index
            if time_value is None:
                if hasattr(index, 'timestamp'):
                    time_value = int(index.timestamp())
                else:
                    try:
                        dt = pd.to_datetime(index)
                        # Check if it has time component
                        if dt.time() != pd.Timestamp('00:00:00').time():
                            # Has time component, use timestamp for intraday data
                            time_value = int(dt.timestamp())
                        else:
                            # Only date, use YYYY-MM-DD format for daily data
                            time_value = dt.strftime('%Y-%m-%d')
                    except:
                        # Use row number as fallback
                        time_value = index

            if time_value is None:
                logger.warning(f"Warning: Could not determine time for row: {row}")
                continue
            
            def _to_float(val, default=0.0):
                if val is None or pd.isna(val):
                    return default
                try:
                    return float(val)
                except (ValueError, TypeError):
                    return default

            point = {
                'time': time_value,
                'open': _to_float(row.get('open', row.get('Open', 0))),
                'high': _to_float(row.get('high', row.get('High', 0))),
                'low': _to_float(row.get('low', row.get('Low', 0))),
                'close': _to_float(row.get('close', row.get('Close', 0))),
            }
            
            # Add price change percentage if calculated
            if price_change_pct is not None:
                idx_pos = df.index.get_loc(index)
                if idx_pos < len(price_change_pct):
                    change_pct = price_change_pct.iloc[idx_pos]
                    if not pd.isna(change_pct):
                        point['changePct'] = float(change_pct)
            
            # Add volume if it exists and has valid data
            if has_volume_data and volume_column:
                volume = _to_float(row.get(volume_column, 0))
                if volume > 0:
                    point['volume'] = volume
                
            chart_data.append(point)
        
        self.chart_data = chart_data
        
        # If the page is already loaded, start the process of setting data.
        # Otherwise, _on_load_finished will handle it.
        if self._page_loaded:
            self._ready_check_attempts = 0
            self._check_chart_ready()
        
        self._update_indicators()
    
    def _on_load_finished(self, success):
        """
        Called when the web page finishes loading.
        """
        logger.info(f"Page load finished: {success}")
        if success:
            self._page_loaded = True
            # Reset check attempts counter when page is loaded/reloaded
            self._ready_check_attempts = 0
            if self.chart_data:
                self._check_chart_ready()
            else:
                # If no data has been set, trigger the initial load for the default period.
                self._handle_period_change('1d')
        else:
            logger.error("Page failed to load")

    def _check_chart_ready(self):
        """
        Polls the web page to see if the chart is ready.
        """
        if not self.web_view or not self._page_loaded:
            return

        if self._ready_check_attempts >= self._max_ready_check_attempts:
            logger.error("Error: Chart readiness check timed out.")
            return

        self._ready_check_attempts += 1
        js_code = "window.chartReady;"
        try:
            self.web_view.page().runJavaScript(js_code, self._handle_chart_ready_result)
        except Exception as e:
            logger.exception(e)

    def _handle_chart_ready_result(self, result):
        """
        Handles the result of the chart readiness check.
        """
        if result is True:
            logger.info("Chart is ready. Setting data.")
            self._ready_check_attempts = 0  # Reset counter on success
            self._set_chart_data()
        else:
            # Chart not ready, check again shortly
            QTimer.singleShot(100, self._check_chart_ready)

    def _set_chart_data(self):
        """
        Set chart data via JavaScript
        """
        if self.chart_data is not None and self.web_view and self._page_loaded:
            # Ensure data is sorted by time
            try:
                sorted_data = sorted(self.chart_data, key=lambda x: x['time'])
            except TypeError:
                logger.warning("Warning: Could not sort chart data due to mixed time types. Using as is.")
                sorted_data = self.chart_data
                
            period = getattr(self, 'current_period', '1d')
            has_volume = getattr(self, 'has_volume', False)
            js_code = f"window.setChartData({json.dumps(sorted_data)}, '{period}', {str(has_volume).lower()});"
            logger.info(f"Running JS with sorted data: {len(sorted_data)} points, hasVolume: {has_volume}")
            try:
                self.web_view.page().runJavaScript(js_code, lambda result: print(f"JS execution result: {result}"))
            except Exception as e:
                logger.exception(e)

    def create_line(self, name: str, color: str = 'blue', width: int = 2):
        """
        Creates a new line series on the chart.
        """
        if self.web_view:
            js_code = f"if (window.chartReady) {{ window.createLine('{name}', '{color}', {width}); }}"
            self.web_view.page().runJavaScript(js_code)

    def update_line(self, name: str, data: pd.DataFrame):
        """
        Updates the data for an existing line series.
        """
        if data is None or data.empty:
            return
        
        # 过滤出有效的value数据
        valid_data = data.dropna(subset=['value']).copy()
        if valid_data.empty:
            return
        
        # 向量化获取时间值
        time_values = self._get_time_values_vectorized(valid_data)
        
        # 构建线条数据
        line_data = [
            {'time': time_val, 'value': float(value)}
            for time_val, value in zip(time_values, valid_data['value'])
            if time_val is not None
        ]
        
        if self.web_view and line_data:
            js_code = f"if (window.chartReady) {{ window.updateLine('{name}', {json.dumps(line_data)}); }}"
            self.web_view.page().runJavaScript(js_code)

    def _get_time_values_vectorized(self, data: pd.DataFrame):
        """
        向量化获取时间值，避免逐行遍历的性能问题
        """
        time_values = []
        
        # 优先使用date列
        if 'date' in data.columns:
            date_series = data['date'].dropna()
            if not date_series.empty:
                # 向量化转换datetime
                try:
                    dt_series = pd.to_datetime(date_series)
                    # 检查是否包含时间组件
                    has_time = (dt_series.dt.time != pd.Timestamp('00:00:00').time()).any()
                    
                    if has_time:
                        # 包含时间组件，使用timestamp
                        time_values = [int(dt.timestamp()) for dt in dt_series]
                    else:
                        # 只有日期，使用字符串格式
                        time_values = [dt.strftime('%Y-%m-%d') for dt in dt_series]
                    
                    # 补充到原始长度
                    result = [None] * len(data)
                    valid_indices = date_series.index
                    for i, idx in enumerate(valid_indices):
                        result[data.index.get_loc(idx)] = time_values[i]
                    return result
                except:
                    pass
        
        # 回退到使用索引
        try:
            if hasattr(data.index, 'to_pydatetime'):
                # DatetimeIndex
                dt_index = data.index.to_pydatetime()
                has_time = any(dt.time() != pd.Timestamp('00:00:00').time() for dt in dt_index[:min(10, len(dt_index))])
                
                if has_time:
                    time_values = [int(dt.timestamp()) for dt in dt_index]
                else:
                    time_values = [dt.strftime('%Y-%m-%d') for dt in dt_index]
            else:
                # 尝试转换索引
                dt_index = pd.to_datetime(data.index)
                has_time = (dt_index.time != pd.Timestamp('00:00:00').time()).any()
                
                if has_time:
                    time_values = [int(dt.timestamp()) for dt in dt_index]
                else:
                    time_values = [dt.strftime('%Y-%m-%d') for dt in dt_index]
        except:
            # 如果所有转换都失败，使用原始索引
            time_values = list(data.index)
        
        return time_values

    def _synthesize_weekly_data(self, daily_df: pd.DataFrame) -> pd.DataFrame:
        """Synthesize weekly data from daily data"""
        if daily_df is None or daily_df.empty:
            return pd.DataFrame()
        
        df = daily_df.copy()
        
        # Ensure we have a proper date column
        if 'date' in df.columns:
            df['datetime'] = pd.to_datetime(df['date'])
        else:
            df['datetime'] = pd.to_datetime(df.index)
        
        df.set_index('datetime', inplace=True)
        
        # Group by week (Monday is the start of week)
        weekly_data = df.groupby(pd.Grouper(freq='W-MON')).agg({
            'open': 'first',
            'high': 'max',
            'low': 'min',
            'close': 'last',
            'volume': 'sum' if 'volume' in df.columns else lambda x: None
        }).dropna(subset=['open', 'high', 'low', 'close'])
        
        # Reset index and add date column
        weekly_data.reset_index(inplace=True)
        weekly_data['date'] = weekly_data['datetime'].dt.strftime('%Y-%m-%d')
        
        return weekly_data

    def _synthesize_monthly_data(self, daily_df: pd.DataFrame) -> pd.DataFrame:
        """Synthesize monthly data from daily data"""
        if daily_df is None or daily_df.empty:
            return pd.DataFrame()
        
        df = daily_df.copy()
        
        # Ensure we have a proper date column
        if 'date' in df.columns:
            df['datetime'] = pd.to_datetime(df['date'])
        else:
            df['datetime'] = pd.to_datetime(df.index)
        
        df.set_index('datetime', inplace=True)
        
        # Group by month
        monthly_data = df.groupby(pd.Grouper(freq='ME')).agg({
            'open': 'first',
            'high': 'max',
            'low': 'min',
            'close': 'last',
            'volume': 'sum' if 'volume' in df.columns else lambda x: None
        }).dropna(subset=['open', 'high', 'low', 'close'])
        
        # Reset index and add date column
        monthly_data.reset_index(inplace=True)
        monthly_data['date'] = monthly_data['datetime'].dt.strftime('%Y-%m-%d')
        
        return monthly_data

    def _synthesize_yearly_data(self, daily_df: pd.DataFrame) -> pd.DataFrame:
        """Synthesize yearly data from daily data"""
        if daily_df is None or daily_df.empty:
            return pd.DataFrame()
        
        df = daily_df.copy()
        
        # Ensure we have a proper date column
        if 'date' in df.columns:
            df['datetime'] = pd.to_datetime(df['date'])
        else:
            df['datetime'] = pd.to_datetime(df.index)
        
        df.set_index('datetime', inplace=True)
        
        # Group by year
        yearly_data = df.groupby(pd.Grouper(freq='Y')).agg({
            'open': 'first',
            'high': 'max',
            'low': 'min',
            'close': 'last',
            'volume': 'sum' if 'volume' in df.columns else lambda x: None
        }).dropna(subset=['open', 'high', 'low', 'close'])
        
        # Reset index and add date column
        yearly_data.reset_index(inplace=True)
        yearly_data['date'] = yearly_data['datetime'].dt.strftime('%Y-%m-%d')
        
        return yearly_data
